﻿// HSS.Interlink.UploadClientAzure.cs
// ----------------------------------------------------------------------------
// Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.html)
// ----------------------------------------------------------------------------
// HighSpeed-Solutions, LLC
// Copyright (c) 2001-2012
// ----------------------------------------------------------------------------
// File:       UploadClientAzure.cs
// Author:     HSS\gbanta
// Created:    12/18/2011
// Modified:   12/21/2011
// ----------------------------------------------------------------------------
namespace HSS.Interlink
{
	#region Using Directives
	using System;
	using System.IO;
	using System.Net;
	using System.Security.Cryptography;
	using System.Threading;
	using System.ComponentModel;
	using System.Text;
	using System.Security;
	using System.Windows;
	using System.Net.Browser;
	using System.Windows.Browser;
	using System.Globalization;
	using System.Collections.Generic;
	using System.Xml;
	using System.Linq;
	using System.Xml.Linq;
	#endregion

	#region UploadClientAzure
	/// <summary>
	/// An <see cref="IUploadClient"/> for Windows Azure Blob Storage.
	/// </summary>
	[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")]
	public class UploadClientAzure : IUploadClient
	{
		#region Delegates
		private delegate void WriteCompletionDelegate(UploadChunkState context);
		private delegate void ReadCompletionDelegate(string response, UploadChunkState context);
		#endregion

		#region Constants
		/// <summary>
		/// Version header tag of request (x-ms-version).
		/// </summary>
		private const string MsVersionHeader = "x-ms-version";
		/// <summary>
		/// Gets the x_ MS_ version (2009-09-19).
		/// </summary>
		private const string HeaderVersion = "2009-09-19";
		#endregion

		#region Events

		#region UploadProgressChanged
		/// <summary>
		/// Occurs when an asynchronous upload operation successfully transfers some or all of the data.
		/// </summary>
		public event EventHandler<UploadFileProgressEventArgs> UploadProgressChanged;
		private void ReportUploadProgressChanged(UploadFileProgressEventArgs args)
		{
			var handler = UploadProgressChanged;
			if (null != handler)
				handler(null, args);
		}
		private void ReportUploadProgressChanged(object arg)
		{
			this.ReportUploadProgressChanged((UploadFileProgressEventArgs)arg);
		}
		#endregion

		#region UploadCompleted
		/// <summary>
		/// Occurs when an asynchronous upload operation has completed.
		/// </summary>
		public event EventHandler<UploadFileCompletedEventArgs> UploadCompleted;
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private void OnUploadCompleted(UploadFileCompletedEventArgs eventArgs)
		{
			var handler = this.UploadCompleted;
			if (null != handler)
			{
				try { handler(this, eventArgs); }
				catch { }
			}
		}
		private void OnUploadAsyncCompleted(object arg)
		{
			this.OnUploadCompleted((UploadFileCompletedEventArgs)arg);
		}
		private void InvokeOperationCompeted(AsyncOperation asyncOp, SendOrPostCallback callback, UploadFileCompletedEventArgs eventArgs)
		{
			if (this.isExpired)
				eventArgs = new UploadFileCompletedEventArgs("", new WebException("Upload expired."), false, this.uploadAsyncOperation.UserSuppliedState);
			if (Interlocked.CompareExchange<AsyncOperation>(ref this.uploadAsyncOperation, null, asyncOp) == asyncOp)
			{
				var originalWebResponse = Interlocked.Exchange<WebResponse>(ref this.webResponse, null);
				CloseResponse(originalWebResponse);
				this.CompleteUploadClientState();
				asyncOp.PostOperationCompleted(callback, eventArgs);
			}
		}
		private void CompleteUploadClientState()
		{
			Interlocked.Decrement(ref this.callNesting);
		}
		#endregion

		#endregion

		#region Fields

		private Uri resolvedUri;
		private ProgressData progressData;
		private WebRequest webRequest;
		private WebResponse webResponse;
		private SendOrPostCallback uploadOperationCompleted;
		private SendOrPostCallback uploadProgressChanged;
		private AsyncOperation uploadAsyncOperation;
		private Timer sasTimer;
		private bool supportsFileOverwrite;
		private bool initUploadClientAsync;
		private bool isCanceled;
		private bool isDone;
		private bool isExpired;
		private int callNesting;

		#endregion

		#region Constructors
		/// <summary>
		/// Constructor
		/// </summary>
		public UploadClientAzure()
		{
		}
		#endregion

		#region Properties

		#region Read-Only
		/// <summary>
		/// Gets the name of the file being uploaded.
		/// </summary>
		public string FileName
		{
			get;
			private set;
		}
		/// <summary>
		/// Gets if the upload is currently in progress.
		/// </summary>
		public bool IsBusy
		{
			get
			{
				return (this.uploadAsyncOperation != null);
			}
		}
		/// <summary>
		/// Gets the azure configuration (Chunk Size and Timeout; timeout=[TIMEOUT_VALUE],maxChunkSize=[MAX_CHUNK_SIZE]).
		/// </summary>
		public string ClientConfig
		{
			get { return _clientConfig; }
			private set
			{
				if (this.IsBusy)
					throw new WebException("Cannot modify the ClientConfig once the upload has begun.");
				if (string.IsNullOrWhiteSpace(value))
					throw new InvalidOperationException("Cannot set the ClientConfig to null or an empty string.");
				_clientConfig = value;
			}
		} string _clientConfig;
		/// <summary>
		/// Gets the uri of the upload service.
		/// </summary>
		public string ServiceUri
		{
			get { return _uploadUri; }
			private set
			{
				if (this.IsBusy)
					throw new WebException("Cannot modify the ServiceUri once the upload has begun.");
				if (string.IsNullOrWhiteSpace(value))
					throw new InvalidOperationException("Cannot set the ServiceUri to null or an empty string.");
				this.resolvedUri = FileQuery.ResolveUrl(value);
				_uploadUri = value;
			}
		} string _uploadUri;
		/// <summary>
		/// Gets the timestamp of when this content began the upload process.
		/// </summary>
		public DateTime StartTimestamp
		{
			get;
			private set;
		}
		/// <summary>
		/// Gets the timestamp of when this content finished the upload process.
		/// </summary>
		public DateTime EndTimestamp
		{
			get;
			private set;
		}
		/// <summary>
		/// Gets the elapsed time of the upload process.
		/// </summary>
		public TimeSpan ElapsedTime
		{
			get
			{
				if (EndTimestamp == StartTimestamp || StartTimestamp == DateTime.MinValue)
					return new TimeSpan(0, 0, 0);
				return EndTimestamp.Subtract(StartTimestamp);
			}
		}
		/// <summary>
		/// Gets the elapsed time of the upload process in Seconds.
		/// </summary>
		public double ElapsedSeconds
		{
			get { return ElapsedTime.TotalSeconds; }
		}
		/// <summary>
		/// Gets the estimated transfer rate (Bytes/Sec) of the upload.
		/// </summary>
		public double TransferRate
		{
			get
			{
				var sec = this.ElapsedSeconds;
				var byt = (double)this.ContentLength;
				if (byt == 0 || sec == 0)
					return 0;
				return byt / sec;
			}
		}
		/// <summary>
		/// Gets the length of the content being uploaded.
		/// </summary>
		public long ContentLength
		{
			get;
			private set;
		}
		#endregion

		#region Read-Write
		/// <summary>
		/// Gets or sets the network credentials that are sent to the host and used to authenticate the request.
		/// </summary>
		/// <value>An <see cref="ICredentials"/> containing the authentication credentials for the request. The default is null.</value>
		public ICredentials Credentials
		{
			get;
			set;
		}
		/// <summary>
		/// Gets or sets a <see cref="Boolean"/> value that controls whether default credentials are sent with requests.
		/// </summary>
		/// <value>true if the default credentials are used; otherwise false. The default value is true.</value>
		public bool UseDefaultCredentials
		{
			get
			{
				return this._useDefaultCredentials;
			}
			set
			{
			}
		} bool _useDefaultCredentials = true;
		#endregion

		#endregion

		#region Methods

		#region Public
		/// <summary>
		/// Initializes the client.
		/// </summary>
		/// <param name="serviceUri">The Uri of the remote service to process the upload request.</param>
		/// <param name="config">The azure configuration; timeout=[TIMEOUT_VALUE],maxChunkSize=[MAX_CHUNK_SIZE].</param>
		/// <param name="allowFileOverwrite">True to overwrite existing files; otherwise False.</param>
		/// <remarks>
		/// <para>
		/// NOTE: The method must be called from the UI Thread. The call to the various upload async methods
		/// does not.
		/// </para>
		/// </remarks>
		public void Initialize(string serviceUri, string config, bool allowFileOverwrite)
		{

			if (!Deployment.Current.Dispatcher.CheckAccess())
				throw new UnauthorizedAccessException("Invalid cross-thread access.");

			if (string.IsNullOrWhiteSpace(serviceUri))
				throw new ArgumentNullException("serviceUri");

			if (string.IsNullOrWhiteSpace(config))
				throw new ArgumentNullException("config");

			this.ServiceUri = serviceUri;
			this.resolvedUri = FileQuery.ResolveUrl(serviceUri);

			this.ClientConfig = config;
			this.supportsFileOverwrite = allowFileOverwrite;
		}
		/// <summary>
		/// Begins uploading the contents.
		/// </summary>
		/// <param name="content">The string to upload.</param>
		/// <param name="fileName">The name of the file the string is being uploaded to.</param>
		public void UploadStringAsync(string content, string fileName)
		{
			this.UploadDataAsync(Encoding.UTF8.GetBytes(content), fileName, string.Empty);
		}
		/// <summary>
		/// Begins uploading the contents.
		/// </summary>
		/// <param name="content">The string to upload.</param>
		/// <param name="fileName">The name of the file the string is being uploaded to.</param>
		/// <param name="metadata">Optional metadata string containing additional information to send with the file.</param>
		public void UploadStringAsync(string content, string fileName, string metadata)
		{
			this.UploadDataAsync(Encoding.UTF8.GetBytes(content), fileName, metadata, null);
		}
		/// <summary>
		/// Begins uploading the contents.
		/// </summary>
		/// <param name="content">The string to upload.</param>
		/// <param name="fileName">The name of the file the string is being uploaded to.</param>
		/// <param name="metadata">Optional metadata string containing additional information to send with the file.</param>
		/// <param name="userToken">Optional user state associated with this request.</param>
		public void UploadStringAsync(string content, string fileName, string metadata, object userToken)
		{
			this.UploadDataAsync(Encoding.UTF8.GetBytes(content), fileName, metadata, userToken);
		}
		/// <summary>
		/// Begins uploading the content.
		/// </summary>
		/// <param name="content">The byte array to upload.</param>
		/// <param name="fileName">The name of the file the bytes are being uploaded to.</param>
		public void UploadDataAsync(byte[] content, string fileName)
		{
			this.UploadDataAsync(content, fileName, string.Empty);
		}
		/// <summary>
		/// Begins uploading the contents.
		/// </summary>
		/// <param name="content">The byte array to upload.</param>
		/// <param name="fileName">The name of the file the bytes are being uploaded to.</param>
		/// <param name="metadata">Optional metadata string containing additional information to send with the file.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
		public void UploadDataAsync(byte[] content, string fileName, string metadata)
		{
			this.UploadStreamAsync(new MemoryStream(content), fileName, metadata, true, null);
		}
		/// <summary>
		/// Begins uploading the contents.
		/// </summary>
		/// <param name="content">The byte array to upload.</param>
		/// <param name="fileName">The name of the file the bytes are being uploaded to.</param>
		/// <param name="metadata">Optional metadata string containing additional information to send with the file.</param>
		/// <param name="userToken">Optional user state associated with this request.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
		public void UploadDataAsync(byte[] content, string fileName, string metadata, object userToken)
		{
			this.UploadStreamAsync(new MemoryStream(content), fileName, metadata, true, userToken);
		}
		/// <summary>
		/// Begins uploading the content.
		/// </summary>
		/// <param name="content">The stream to upload.</param>
		/// <param name="fileName">The name of the file the stream is being uploaded to.</param>
		public void UploadStreamAsync(Stream content, string fileName)
		{
			this.UploadStreamAsync(content, fileName, string.Empty, true);
		}
		/// <summary>
		/// Begins uploading the content.
		/// </summary>
		/// <param name="content">The stream to upload.</param>
		/// <param name="fileName">The name of the file the stream is being uploaded to.</param>
		/// <param name="metadata">Optional metadata string containing additional information to send with the file.</param>
		/// <param name="autoCloseStream">True to close the provided content stream once the upload completes; otherwise false to leave open.</param>
		public void UploadStreamAsync(Stream content, string fileName, string metadata, bool autoCloseStream)
		{
			this.UploadStreamAsync(content, fileName, metadata, autoCloseStream, null);
		}
		/// <summary>
		/// Begins uploading the content.
		/// </summary>
		/// <param name="content">The stream to upload.</param>
		/// <param name="fileName">The name of the file the stream is being uploaded to.</param>
		/// <param name="metadata">Optional metadata string containing additional information to send with the file.</param>
		/// <param name="autoCloseStream">True to close the provided content stream once the upload completes; otherwise false to leave open.</param>
		/// <param name="userToken">Optional user state associated with this request.</param>
		public void UploadStreamAsync(Stream content, string fileName, string metadata, bool autoCloseStream, object userToken)
		{
			if (null == content)
				throw new ArgumentNullException("content");

			if (!content.CanRead)
				throw new ArgumentException("The provided content Stream is not readable.", "content");

			if (!content.CanSeek)
				throw new ArgumentException("The provided content Stream is not seekable.", "content");

			if (content.Length < 1)
				throw new ArgumentException("Content length is zero (0); nothing to send.", "content");

			if (content.Length == content.Position)
				throw new ArgumentException("Content position is at the end of the file; no bytes left to send.", "content");

			if (string.IsNullOrWhiteSpace(fileName))
				throw new ArgumentNullException("fileName");

			this.InitUploadClientState();
			this.ClearUploadClientState();

			this.FileName = Path.GetFileName(fileName);
			this.ContentLength = content.Length;

			// Azure Uri
			var uriBuilder = new UriBuilder(resolvedUri);
			uriBuilder.Path += string.Format(CultureInfo.InvariantCulture, "/{0}", this.FileName);
			this.resolvedUri = uriBuilder.Uri;

			var asyncOp = AsyncOperationManager.CreateOperation(userToken);
			this.uploadAsyncOperation = asyncOp;

			try
			{
				if (!string.IsNullOrEmpty(metadata))
					metadata = HttpUtility.HtmlEncode(metadata);
				var request = this.GetWebRequest(Strings.HTTP_HEAD, "metadata");
				request.Headers[MsVersionHeader] = HeaderVersion;
				var asyncState = new UploadChunkState(request, content, metadata, autoCloseStream, this.AppendChunkWriteCallback, this.AppendChunkReadCallback, asyncOp, this.progressData, this);
				request.BeginGetResponse(this.SendFileCheckResponse, asyncState);
			}
			catch (Exception exception)
			{
				if (((exception is ThreadAbortException) || (exception is StackOverflowException)) || (exception is OutOfMemoryException))
				{
					throw;
				}
				ProcessException(ref exception, exception);
				if (null != content)
					content.Close();
				var eventArgs = new UploadFileCompletedEventArgs(null, exception, this.isCanceled, asyncOp.UserSuppliedState);
				this.InvokeOperationCompeted(asyncOp, this.uploadOperationCompleted, eventArgs);
			}
		}
		/// <summary>
		/// Request to cancel the write operation
		/// </summary>
		public void CancelAsync()
		{
			if (this.isCanceled)
				return;
			if (this.isDone)
				return;
			var req = this.webRequest;
			this.isCanceled = true;
			AbortRequest(req);
		}
		#endregion

		#region Private

		#region Pipeline

		#region FileExist Response
		void SendFileCheckResponse(IAsyncResult result)
		{
			#region State
			var asyncState = result.AsyncState as UploadChunkState;
			var req = asyncState.Request;
			var headers = default(WebHeaderCollection);
			#endregion

			try
			{
				#region Get Response Headers

				using (var resp = this.GetWebResponse(req, result))
					headers = resp.Headers;

				#endregion
			}
			catch (Exception exception2)
			{
				#region Handle Exceptions

				if (((exception2 is ThreadAbortException) || (exception2 is StackOverflowException)) || (exception2 is OutOfMemoryException))
					throw;

				#endregion
			}
			finally
			{
				#region Process Response

				if (this.isCanceled)
				{
					this.SendCancel(asyncState);
				}
				else
				{
					// If we were able to get the headers, then we assume the file exists.
					if (null != headers && !this.supportsFileOverwrite)
					{
						// File already exists...
						asyncState.Exception = new WebException(Strings.Ex_FileAlreadyExists);
						this.SendCancel(asyncState);
					}
					else
					{
						// Begin uploading...
						this.StartTimestamp = DateTime.Now;
						this.AppendChunk(asyncState);
					}
				}

				#endregion
			}
		}
		#endregion

		#region Append Chunk
		private void AppendChunk(UploadChunkState asyncState)
		{
			try
			{
				if (this.isCanceled)
				{
					this.SendCancel(asyncState);
					return;
				}
				var blockId = asyncState.GetNextBlockId();
				asyncState.FillBuffer();
				var request = asyncState.Request = this.GetWebRequest(Strings.HTTP_PUT, blockId);
				request.BeginGetRequestStream(UploadClientAzure.UploadChunkRequestCallback, asyncState);
			}
			catch (Exception exception)
			{
				if (((exception is ThreadAbortException) || (exception is StackOverflowException)) || (exception is OutOfMemoryException))
					throw;
				if (!(exception is WebException) && !(exception is SecurityException))
					exception = new WebException(Strings.Ex_UnknownError, exception);
				asyncState.Exception = exception;
				this.SendCancel(asyncState);
			}
		}
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private void AppendChunkWriteCallback(UploadChunkState asyncState)
		{
			var ex = default(Exception);
			ProcessException(ref ex, asyncState.Exception);

			if (ex != null)
			{
				asyncState.Exception = ex;
				this.SendCancel(asyncState);
			}
			else
			{
				try
				{
					webRequest.BeginGetResponse(AppendChunkGetResponse, asyncState);
				}
				catch (Exception ex2)
				{
					ProcessException(ref ex, ex2);
					asyncState.Exception = ex;
					this.SendCancel(asyncState);
				}
			}
		}
		private void AppendChunkReadCallback(string response, UploadChunkState asyncState)
		{
			var ex = default(Exception);
			ProcessException(ref ex, asyncState.Exception);

			if (ex != null)
			{
				asyncState.Exception = ex;
				this.SendCancel(asyncState);
			}
			else if (response == Responses.AppendFileRetry.ToString())
			{
				if (this.isCanceled)
				{
					asyncState.Exception = ex;
					this.SendCancel(asyncState);
				}
				else
				{
					AppendChunkRetry(asyncState);
				}
			}
			else if (this.progressData.BytesSent != this.progressData.TotalBytesToSend)
			{
				if (this.isCanceled)
				{
					asyncState.Exception = ex;
					this.SendCancel(asyncState);
				}
				else
				{
					this.AppendChunk(asyncState);
				}
			}
			else
			{
				this.isDone = true;
				this.SendComplete(asyncState);
			}
		}
		private static void AppendChunkRetry(UploadChunkState asyncState)
		{
			asyncState.Progress.BytesSent -= asyncState.Progress.LastBytesSent;
			asyncState.AzureClient.AppendChunk(asyncState);
		}
		private static void AppendChunkGetResponse(IAsyncResult result)
		{
			#region State
			var asyncState = result.AsyncState as UploadChunkState;
			var response = string.Empty;
			#endregion

			try
			{
				#region Get Response
				var webRequest = asyncState.Request as HttpWebRequest;
				if (webRequest.HaveResponse && result.IsCompleted)
				{
					asyncState.ReceivedResponse();
				}
				#endregion
			}
			catch (Exception exception2)
			{
				#region Handle Exceptions

				if (((exception2 is ThreadAbortException) || (exception2 is StackOverflowException)) || (exception2 is OutOfMemoryException))
					throw;

				// Retry!!
				response = Responses.AppendFileRetry.ToString();

				#endregion
			}
			finally
			{
				asyncState.ReadCompletionDelegate(response, asyncState);
			}
		}
		private static void UploadChunkRequestCallback(IAsyncResult result)
		{
			var asyncState = (UploadChunkState)result.AsyncState;
			var request = asyncState.Request;
			var exception = default(Exception);

			try
			{
				var writeStream = request.EndGetRequestStream(result);
				asyncState.SetWriteStream(writeStream);
			}
			catch (Exception exception2)
			{
				if (((exception2 is ThreadAbortException) || (exception2 is StackOverflowException)) || (exception2 is OutOfMemoryException))
					throw;
				exception = exception2;
				if (!(exception2 is WebException) && !(exception2 is SecurityException))
					exception = new WebException(Strings.Ex_UnknownError, exception2);
				AbortRequest(request);
				asyncState.Exception = exception;
				if (asyncState.ReadStream != null)
					asyncState.ReadStream.Close();
			}
			finally
			{
				if (exception != null)
					asyncState.WriteCompletionDelegate(asyncState);
			}
		}
		private static void UploadChunkWriteCallback(IAsyncResult result)
		{
			var asyncState = (UploadChunkState)result.AsyncState;
			var writeStream = asyncState.WriteStream;
			var exception = default(Exception);

			try
			{
				writeStream.EndWrite(result);
				asyncState.WriteBytes();
			}
			catch (Exception exception2)
			{
				if (((exception2 is ThreadAbortException) || (exception2 is StackOverflowException)) || (exception2 is OutOfMemoryException))
					throw;
				exception = exception2;
				if (!(exception2 is WebException) && !(exception2 is SecurityException))
					exception = new WebException(Strings.Ex_UnknownError, exception2);
				AbortRequest(asyncState.Request);
				asyncState.Exception = exception;
				if (asyncState.ReadStream != null)
					asyncState.ReadStream.Close();
			}
			finally
			{
				asyncState.WriteCompletionDelegate(asyncState);
			}
		}
		#endregion

		#region Send Cancel
		void SendCancel(UploadChunkState asyncState)
		{
			this.EndTimestamp = DateTime.Now;

			// Nothing to cancel, so we just end.
			this.UploadFileCompleted(asyncState, string.Empty, this.isCanceled, asyncState.Exception);
		}
		#endregion

		#region Send Complete
		void SendComplete(UploadChunkState asyncState)
		{
			#region State
			this.isDone = true;
			var request = default(WebRequest);
			#endregion

			#region Send Completed
			try
			{
				this.EndTimestamp = DateTime.Now;
				request = asyncState.Request = this.GetWebRequest(Strings.HTTP_PUT, "blocklist");
				request.Headers[MsVersionHeader] = HeaderVersion;
				request.BeginGetRequestStream(this.SendCompleteRequest, asyncState);
			}
			catch (Exception exception)
			{
				if (((exception is ThreadAbortException) || (exception is StackOverflowException)) || (exception is OutOfMemoryException))
					throw;
				if (null != request)
					request.Abort();
				asyncState.Exception = exception;
				this.SendCancel(asyncState);
			}
			#endregion
		}
		void SendCompleteRequest(IAsyncResult result)
		{
			#region State
			var asyncState = result.AsyncState as UploadChunkState;
			var req = asyncState.Request;
			var requestStream = default(Stream);
			var exception = default(Exception);
			#endregion

			try
			{
				#region Write BlockList

				requestStream = req.EndGetRequestStream(result);
				var writer = XmlWriter.Create(requestStream, new XmlWriterSettings() { Encoding = Encoding.UTF8 });
				UploadClientAzure.UncommittedBlockBlobList(asyncState.BlockIds).Save(writer);
				writer.Flush();
				requestStream.Close();
				req.BeginGetResponse(this.SendCompleteResponse, asyncState);

				#endregion
			}
			catch (Exception exception2)
			{
				#region Handle Exceptions

				if (((exception2 is ThreadAbortException) || (exception2 is StackOverflowException)) || (exception2 is OutOfMemoryException))
					throw;

				ProcessException(ref exception, exception2);

				return;

				#endregion
			}
		}
		void SendCompleteResponse(IAsyncResult result)
		{
			#region State
			var asyncState = result.AsyncState as UploadChunkState;
			var req = asyncState.Request;
			var responseStream = default(Stream);
			var exception = default(Exception);
			#endregion

			try
			{
				#region Get Response

				responseStream = this.GetWebResponse(req, result).GetResponseStream();
				using (var stm = new StreamReader(responseStream))
					stm.ReadToEnd();

				#endregion
			}
			catch (Exception exception2)
			{
				#region Handle Exceptions

				if (((exception2 is ThreadAbortException) || (exception2 is StackOverflowException)) || (exception2 is OutOfMemoryException))
					throw;

				ProcessException(ref exception, exception2);

				#endregion
			}

			#region Process Response

			if (null != exception)
			{
				asyncState.Exception = exception;
				this.SendCancel(asyncState);
			}
			else
			{
				if (!string.IsNullOrEmpty(asyncState.Metadata) && asyncState.Metadata.Contains("=") && asyncState.Metadata.Contains("x-ms-meta-"))
					this.SetMetadata(asyncState);
				else
					this.UploadFileCompleted(asyncState, string.Empty, false, null);
			}

			#endregion
		}
		#endregion

		#region SetMetadata
		private void SetMetadata(UploadChunkState asyncState)
		{
			#region State
			this.isDone = true;
			var request = default(WebRequest);
			#endregion

			#region Set Metadata
			try
			{
				request = asyncState.Request = this.GetWebRequest(Strings.HTTP_PUT, "metadata");
				request.Headers[MsVersionHeader] = HeaderVersion;

				var meta = asyncState.Metadata.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
				foreach (var item in meta)
				{
					var kvp = item.Split("=".ToCharArray());
					var k = kvp[0];
					var v = kvp[1];
					if (k.StartsWith("x-ms-meta-", StringComparison.OrdinalIgnoreCase))
						request.Headers[k] = v;
				}
				request.BeginGetResponse(this.SetMetadataResponse, asyncState);
			}
			catch (Exception exception)
			{
				if (((exception is ThreadAbortException) || (exception is StackOverflowException)) || (exception is OutOfMemoryException))
					throw;
				this.UploadFileCompleted(asyncState, string.Empty, false, null);
			}
			#endregion
		}
		private void SetMetadataResponse(IAsyncResult result)
		{
			#region State
			var asyncState = result.AsyncState as UploadChunkState;
			var req = asyncState.Request;
			var responseStream = default(Stream);
			#endregion

			try
			{
				#region Get Response

				responseStream = this.GetWebResponse(req, result).GetResponseStream();
				using (var stm = new StreamReader(responseStream))
					stm.ReadToEnd();

				#endregion
			}
			catch (Exception exception2)
			{
				#region Handle Exceptions

				if (((exception2 is ThreadAbortException) || (exception2 is StackOverflowException)) || (exception2 is OutOfMemoryException))
					throw;

				#endregion
			}

			this.UploadFileCompleted(asyncState, string.Empty, false, null);
		}
		#endregion

		#endregion

		#region Helpers

		private void InitUploadClientState()
		{
			if (!this.initUploadClientAsync)
			{
				this.uploadOperationCompleted = new SendOrPostCallback(this.OnUploadAsyncCompleted);
				this.uploadProgressChanged = new SendOrPostCallback(this.ReportUploadProgressChanged);
				this.progressData = new ProgressData();
				this.initUploadClientAsync = true;
			}
		}
		private void ClearUploadClientState()
		{
			if (AnotherCallInProgress(Interlocked.Increment(ref this.callNesting)))
			{
				this.CompleteUploadClientState();
				throw new NotSupportedException("Concurrent upload requests are not supported.");
			}
			this.webRequest = null;
			this.webResponse = null;
			this.isCanceled = false;

			if (this.progressData != null)
				this.progressData.Reset();
		}
		private static bool AnotherCallInProgress(int nesting)
		{
			return (nesting > 1);
		}
		private static void ProcessException(ref Exception exception, Exception exception2)
		{
			if (null == exception2)
			{
				exception = null;
				return;
			}

			exception = exception2;
			var wex = exception2 as WebException;
			var sex = exception2 as SecurityException;
			if (null == wex && null == sex)
			{
				exception = new WebException(Strings.Ex_UnknownError, exception2);
			}
			if (null != wex)
			{
				if (wex.Status == WebExceptionStatus.RequestCanceled)
					exception = null;
			}
		}
		private void PostProgressChanged(AsyncOperation asyncOp, ProgressData progress)
		{
			if ((asyncOp != null) && progress.TotalBytesToSend > 0)
			{
				var pct = (int)((progress.BytesSent * 100) / progress.TotalBytesToSend);
				asyncOp.Post(this.uploadProgressChanged, new UploadFileProgressEventArgs(pct, asyncOp.UserSuppliedState, progress.BytesSent, progress.TotalBytesToSend));
			}
		}
		private static void AbortRequest(WebRequest request)
		{
			try
			{
				if (request != null)
					request.Abort();
			}
			catch (Exception exception)
			{
				if (((exception is OutOfMemoryException) || (exception is StackOverflowException)) || (exception is ThreadAbortException))
				{
					throw;
				}
			}
		}
		private static void CloseResponse(WebResponse response)
		{
			try
			{
				if (response != null)
					response.Close();
			}
			catch (Exception exception)
			{
				if (((exception is OutOfMemoryException) || (exception is StackOverflowException)) || (exception is ThreadAbortException))
				{
					throw;
				}
			}
		}
		private HttpWebRequest GetWebRequest(string requestMethod, string command = "")
		{
			var originalWebResponse = Interlocked.Exchange<WebResponse>(ref this.webResponse, null);
			CloseResponse(originalWebResponse);

			var uri = this.GetRequestUri(command);
			var request = (HttpWebRequest)WebRequestCreator.ClientHttp.Create(uri);
			request.Method = requestMethod.ToUpperInvariant();
			request.ContentType = "";
			var originalWebRequest = Interlocked.Exchange<WebRequest>(ref this.webRequest, request);
			if (null != originalWebRequest)
			{
				originalWebRequest.Abort();
				originalWebRequest = null;
			}
			return request;

		}
		private WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
		{
			var response = request.EndGetResponse(result);
			var originalWebResponse = Interlocked.Exchange<WebResponse>(ref this.webResponse, response);
			if (null != originalWebResponse)
			{
				originalWebResponse.Close();
				originalWebResponse = null;
			}
			return response;
		}
		private Uri GetRequestUri(string command)
		{
			if (string.IsNullOrEmpty(command))
				return this.resolvedUri;

			var uriBuilder = new UriBuilder(this.resolvedUri);

			if (command == "blocklist")
			{
				uriBuilder.Query = uriBuilder.Query.TrimStart('?') + "&comp=blocklist";
				return uriBuilder.Uri;
			}

			if (command == "metadata")
			{
				uriBuilder.Query = uriBuilder.Query.TrimStart('?') + "&comp=metadata";
				return uriBuilder.Uri;
			}

			uriBuilder.Query = uriBuilder.Query.TrimStart('?') + string.Format(CultureInfo.InvariantCulture, "&comp=block&blockid={0}", command);
			return uriBuilder.Uri;
		}
		private void UploadFileCompleted(UploadChunkState asyncState, string result, bool cancelled, Exception exception)
		{
			asyncState.Close();
			var eventArgs = new UploadFileCompletedEventArgs(result, exception, cancelled, this.uploadAsyncOperation.UserSuppliedState);
			this.InvokeOperationCompeted(this.uploadAsyncOperation, this.uploadOperationCompleted, eventArgs);
		}
		private void MonitorSASExpiration(int timeout)
		{
			sasTimer = new Timer((o) =>
			{
				this.isExpired = true;
				this.CancelAsync();
				sasTimer.Dispose();
			});
			sasTimer.Change(timeout * 1000, Timeout.Infinite);
		}
		private static XDocument UncommittedBlockBlobList(List<string> blockList)
		{
			var blocks = from blockId in blockList
						 select new XElement("Uncommitted", blockId);
			return new XDocument(new XElement("BlockList", blocks));
		}

		#endregion

		#endregion

		#endregion

		#region ProgressData
		private class ProgressData
		{
			#region Fields
			internal long LastBytesSent;
			internal long BytesSent;
			internal long TotalBytesToSend = -1L;
			#endregion

			#region Constructors
			internal ProgressData()
			{

			}
			#endregion

			#region Methods
			internal void Reset()
			{
				this.LastBytesSent = 0L;
				this.BytesSent = 0L;
				this.TotalBytesToSend = -1L;
			}
			#endregion
		}
		#endregion

		#region UploadChunkState
		private sealed class UploadChunkState
		{
			#region Constants

			internal const int KB1 = 0x400;				// 1024 bytes

			#endregion

			#region Fields


			internal Exception Exception;
			internal AsyncOperation AsyncOp;
			internal WriteCompletionDelegate WriteCompletionDelegate;
			internal ReadCompletionDelegate ReadCompletionDelegate;
			internal UploadClientAzure.ProgressData Progress;
			internal Stream ReadStream;
			internal Stream WriteStream;
			internal WebRequest Request;
			internal UploadClientAzure AzureClient;
			internal List<string> BlockIds;
			internal byte[] InnerBuffer;
			internal string Metadata;
			internal bool isClosed;

			private bool autoCloseReadStream;
			private int currentChunk;
			private int chunkSize = KB1 * KB1;		// 1KB - Default size; changed on the fly based on Server's MaxRequestLength

			#endregion

			#region Constructors
			[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
			internal UploadChunkState(WebRequest request, Stream readStream, string metadata, bool autoCloseContent, WriteCompletionDelegate writeCompletionDelegate, ReadCompletionDelegate readCompletionDelegate, AsyncOperation asyncOperation, UploadClientAzure.ProgressData progress, UploadClientAzure webClient)
			{
				this.Metadata = metadata;
				this.autoCloseReadStream = autoCloseContent;
				this.BlockIds = new List<string>();
				this.AsyncOp = asyncOperation;
				this.ReadStream = readStream;
				this.Request = request;
				this.WriteCompletionDelegate = writeCompletionDelegate;
				this.ReadCompletionDelegate = readCompletionDelegate;
				if (this.AsyncOp != null)
				{
					this.Progress = progress;
					this.Progress.TotalBytesToSend = readStream.Length - readStream.Position;
					this.Progress.BytesSent = readStream.Position;
				}
				this.AzureClient = webClient;

				if (!string.IsNullOrEmpty(webClient.ClientConfig))
				{
					try
					{
						var configItems = new Queue<string>(webClient.ClientConfig.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries));
						while (configItems.Count > 0)
						{
							var item = configItems.Dequeue();
							var kvp = item.Split("=".ToCharArray());
							var k = kvp[0];
							var v = kvp[1];
							if (string.IsNullOrEmpty(v))
								continue;
							switch (k)
							{
								case "maxChunkSize":
									this.SetMaxChunkSize(int.Parse(v, System.Globalization.CultureInfo.InvariantCulture));
									break;

								case "timeout":
									webClient.MonitorSASExpiration(int.Parse(v, System.Globalization.CultureInfo.InvariantCulture));
									break;
							}
						}
					}
					catch { }
				}
			}
			#endregion

			#region Properties
			internal bool Async
			{
				get
				{
					return (this.AsyncOp != null);
				}
			}
			#endregion

			#region Methods
			[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
			internal void Close()
			{
				if (isClosed)
					return;
				isClosed = true;
				if (null != this.WriteStream && !AzureClient.isCanceled)
				{
					try { this.WriteStream.Close(); }
					catch { }
				}
				this.CloseReadStream();
			}
			[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
			internal void CloseReadStream(bool forceClose = false)
			{
				if (null != this.ReadStream)
				{
					if (this.autoCloseReadStream || forceClose)
					{
						try { this.ReadStream.Close(); }
						catch { }
					}
				}
			}
			internal void SetMaxChunkSize(int newChunkSize)
			{

				this.chunkSize = newChunkSize;
			}
			internal string GetNextBlockId()
			{
				var blockId = this.BlockIds.Count + 1;
				var blockSerial = Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0:D4}", blockId)));
				this.BlockIds.Add(blockSerial);
				return blockSerial;
			}
			internal int FillBuffer()
			{
				this.ReadStream.Position = this.Progress.BytesSent;
				this.Progress.LastBytesSent = 0;

				// Calculate current chunk
				var bytesLeftToSend = this.Progress.TotalBytesToSend - this.Progress.BytesSent;
				this.currentChunk = this.chunkSize;
				if (this.currentChunk > bytesLeftToSend)
					this.currentChunk = (Int32)bytesLeftToSend;

				// Read and hash current chunk
				var bufferLen = this.currentChunk;
				var buffer = new byte[bufferLen];
				this.currentChunk = this.ReadStream.Read(buffer, 0, this.currentChunk);

				// Fill inner buffer
				bufferLen = this.currentChunk;
				this.InnerBuffer = new byte[bufferLen];
				Buffer.BlockCopy(buffer, 0, this.InnerBuffer, 0, bufferLen);

				return bufferLen;
			}
			internal void SetWriteStream(Stream writeStream)
			{
				this.WriteStream = writeStream;
				this.Progress.BytesSent += this.currentChunk;
				this.WriteStream.BeginWrite(this.InnerBuffer, 0, this.InnerBuffer.Length, new AsyncCallback(UploadClientAzure.UploadChunkWriteCallback), this);
			}
			internal void WriteBytes()
			{
				if (this.Async)
				{
					this.Progress.LastBytesSent = this.currentChunk;
					this.AzureClient.PostProgressChanged(this.AsyncOp, this.Progress);
				}
				this.WriteStream.Close();
			}
			internal void ReceivedResponse()
			{
				if (this.Async)
				{
					this.AzureClient.PostProgressChanged(this.AsyncOp, this.Progress);
				}
			}
			#endregion
		}
		#endregion

		#region Responses
		/// <summary>
		/// Responses from the Server.
		/// </summary>
		struct Responses
		{
			/// <summary>
			/// append-file-retry
			/// </summary>
			public const string AppendFileRetry = "append-file-retry";
			/// <summary>
			/// file-already-exists
			/// </summary>
			public const string FileAlreadyExists = "file-already-exists";
			/// <summary>
			/// file-does-not-exist
			/// </summary>
			public const string FileDoesNotExist = "file-does-not-exist";
		}
		#endregion
	}
	#endregion
}